<script setup>
import {
  cloneDeep,
  flatMap,
  get,
  isEmpty,
  map,
  mapValues,
  pickBy,
  random,
  slice,
  split,
  sum,
  toArray,
  toNumber,
} from 'lodash'
import { computed, h, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue'
import Refresh from '@/components/icons/Refresh.vue'
import useBrowserStore from 'stores/browser.js'
import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
import { NButton, NIcon, NSpace, useThemeVars } from 'naive-ui'
import { Line } from 'vue-chartjs'
import dayjs from 'dayjs'
import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
import usePreferencesStore from 'stores/preferences.js'
import { useI18n } from 'vue-i18n'
import useConnectionStore from 'stores/connections.js'
import { toHumanReadable } from '@/utils/date.js'

const props = defineProps({
  server: String,
  pause: Boolean,
})

const browserStore = useBrowserStore()
const prefStore = usePreferencesStore()
const connectionStore = useConnectionStore()
const i18n = useI18n()
const themeVars = useThemeVars()
const serverInfo = ref({})
const pageState = reactive({
  autoRefresh: false,
  refreshInterval: 5,
  loading: false, // loading status for refresh
  autoLoading: false, // loading status for auto refresh
})
const statusHistory = 5

/**
 *
 * @param origin
 * @param {string[]} [labels]
 * @param {number[][]} [datalist]
 * @return {unknown}
 */
const generateData = (origin, labels, datalist) => {
  let ret = toRaw(origin)
  ret.labels = labels || ret.labels
  if (datalist && datalist.length > 0) {
    for (let i = 0; i < datalist.length; i++) {
      ret.datasets[i].data = datalist[i]
    }
  }
  return cloneDeep(ret)
}

/**
 * refresh server status info
 * @param {boolean} [force] force refresh will show loading indicator
 * @returns {Promise<void>}
 */
const refreshInfo = async (force) => {
  if (force) {
    pageState.loading = true
  } else {
    pageState.autoLoading = true
  }
  if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
    try {
      const info = await browserStore.getServerInfo(props.server)
      serverInfo.value = info
      _updateChart(info)
    } finally {
      pageState.loading = false
      pageState.autoLoading = false
    }
  }
}

const _updateChart = (info) => {
  let timeLabels = toRaw(cmdRate.value.labels)
  timeLabels = timeLabels.concat(dayjs().format('hh:mm:ss'))
  timeLabels = slice(timeLabels, Math.max(0, timeLabels.length - statusHistory))

  // commands per seconds
  {
    let dataset = toRaw(cmdRate.value.datasets[0].data)
    const cmd = parseInt(get(info, 'Stats.instantaneous_ops_per_sec', '0'))
    dataset = dataset.concat(cmd)
    dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
    cmdRate.value = generateData(cmdRate.value, timeLabels, [dataset])
  }

  // connected clients
  {
    let dataset = toRaw(connectedClients.value.datasets[0].data)
    const count = parseInt(get(info, 'Clients.connected_clients', '0'))
    dataset = dataset.concat(count)
    dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
    connectedClients.value = generateData(connectedClients.value, timeLabels, [dataset])
  }

  // memory usage
  {
    let dataset = toRaw(memoryUsage.value.datasets[0].data)
    let size = parseInt(get(info, 'Memory.used_memory', '0'))
    dataset = dataset.concat(size)
    dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
    memoryUsage.value = generateData(memoryUsage.value, timeLabels, [dataset])
  }

  // network input/output rate
  {
    let dataset1 = toRaw(networkRate.value.datasets[0].data)
    const input = parseInt(get(info, 'Stats.instantaneous_input_kbps', '0'))
    dataset1 = dataset1.concat(input)
    dataset1 = slice(dataset1, Math.max(0, dataset1.length - statusHistory))

    let dataset2 = toRaw(networkRate.value.datasets[1].data)
    const output = parseInt(get(info, 'Stats.instantaneous_output_kbps', '0'))
    dataset2 = dataset2.concat(output)
    dataset2 = slice(dataset2, Math.max(0, dataset2.length - statusHistory))
    networkRate.value = generateData(networkRate.value, timeLabels, [dataset1, dataset2])
  }
}

/**
 * for mock activity data only
 * @private
 */
const _mockChart = () => {
  const timeLabels = []
  for (let i = 0; i < 5; i++) {
    timeLabels.push(dayjs().add(5, 'seconds').format('hh:mm:ss'))
  }

  // commands per seconds
  {
    const dataset = []
    for (let i = 0; i < 5; i++) {
      dataset.push(random(10, 200))
    }
    cmdRate.value = generateData(cmdRate.value, timeLabels, [dataset])
  }

  // connected clients
  {
    const dataset = []
    for (let i = 0; i < 5; i++) {
      dataset.push(random(10, 20))
    }
    connectedClients.value = generateData(connectedClients.value, timeLabels, [dataset])
  }

  // memory usage
  {
    const dataset = []
    for (let i = 0; i < 5; i++) {
      dataset.push(random(120 * 1024 * 1024, 200 * 1024 * 1024))
    }
    memoryUsage.value = generateData(memoryUsage.value, timeLabels, [dataset])
  }

  // network input/output rate
  {
    const dataset1 = []
    for (let i = 0; i < 5; i++) {
      dataset1.push(random(100, 1500))
    }

    const dataset2 = []
    for (let i = 0; i < 5; i++) {
      dataset2.push(random(200, 3000))
    }

    networkRate.value = generateData(networkRate.value, timeLabels, [dataset1, dataset2])
  }
}

const isLoading = computed(() => {
  return pageState.loading || pageState.autoLoading
})

const startAutoRefresh = async () => {
  // connectionStore.getRefreshInterval()
  let lastExec = Date.now()
  do {
    if (!pageState.autoRefresh) {
      break
    }
    await timeout(100)
    if (
      props.pause ||
      pageState.loading ||
      pageState.autoLoading ||
      Date.now() - lastExec < pageState.refreshInterval * 1000
    ) {
      continue
    }
    lastExec = Date.now()
    await refreshInfo()
  } while (true)
  stopAutoRefresh()
}

const stopAutoRefresh = () => {
  pageState.autoRefresh = false
}

const onToggleRefresh = (on) => {
  if (on) {
    tabVal.value = 'activity'
    connectionStore.saveRefreshInterval(props.server, pageState.refreshInterval || 5)
    startAutoRefresh()
  } else {
    connectionStore.saveRefreshInterval(props.server, -1)
    stopAutoRefresh()
  }
}

onMounted(() => {
  const interval = connectionStore.getRefreshInterval(props.server)
  if (interval >= 0) {
    pageState.autoRefresh = true
    pageState.refreshInterval = interval === 0 ? 5 : interval
    onToggleRefresh(true)
  } else {
    setTimeout(refreshInfo, 3000)
    // setTimeout(_mockChart, 1000)
  }
  refreshInfo()
})

onUnmounted(() => {
  stopAutoRefresh()
})

const redisVersion = computed(() => {
  return get(serverInfo.value, 'Server.redis_version', '')
})

const redisMode = computed(() => {
  return get(serverInfo.value, 'Server.redis_mode', '')
})

const role = computed(() => {
  return get(serverInfo.value, 'Replication.role', '')
})

const timeUnit = ['common.unit_minute', 'common.unit_hour', 'common.unit_day']
const uptime = computed(() => {
  let seconds = parseInt(get(serverInfo.value, 'Server.uptime_in_seconds', '0'))
  seconds /= 60
  if (seconds < 60) {
    // minutes
    return { value: Math.floor(seconds), unit: timeUnit[0] }
  }
  seconds /= 60
  if (seconds < 60) {
    // hours
    return { value: Math.floor(seconds), unit: timeUnit[1] }
  }
  return { value: Math.floor(seconds / 24), unit: timeUnit[2] }
})

const usedMemory = computed(() => {
  let size = parseInt(get(serverInfo.value, 'Memory.used_memory', '0'))
  const { value, unit } = convertBytes(size)
  return [value, unit]
})

const totalKeys = computed(() => {
  const regex = /^db\d+$/
  const result = pickBy(serverInfo.value['Keyspace'], (value, key) => {
    return regex.test(key)
  })
  const nums = mapValues(result, (v) => {
    const keys = split(v, ',', 1)[0]
    const num = split(keys, '=', 2)[1]
    return toNumber(num)
  })
  return sum(toArray(nums))
})

const tabVal = ref('activity')
const infoFilter = reactive({
  keyword: '',
  group: 'CPU',
})

const info = computed(() => {
  if (!isEmpty(infoFilter.group)) {
    const val = serverInfo.value[infoFilter.group]
    if (!isEmpty(val)) {
      return map(val, (v, k) => ({
        key: k,
        value: v,
      }))
    }
  }

  return flatMap(serverInfo.value, (value, key) => {
    return map(value, (v, k) => ({
      group: key,
      key: k,
      value: v,
    }))
  })
})

const onFilterGroup = (group) => {
  if (group === infoFilter.group) {
    infoFilter.group = ''
  } else {
    infoFilter.group = group
  }
}

watch(
  () => prefStore.currentLanguage,
  () => {
    // force update labels of charts
    cmdRate.value.datasets[0].label = i18n.t('status.act_cmd')
    cmdRate.value = generateData(cmdRate.value)
    connectedClients.value.datasets[0].label = i18n.t('status.connected_clients')
    connectedClients.value = generateData(connectedClients.value)
    memoryUsage.value.datasets[0].label = i18n.t('status.memory_used')
    memoryUsage.value = generateData(memoryUsage.value)
    networkRate.value.datasets[0].label = i18n.t('status.act_network_input')
    networkRate.value.datasets[1].label = i18n.t('status.act_network_output')
    networkRate.value = generateData(networkRate.value)
  },
)

const chartBGColor = [
  'rgba(255, 99, 132, 0.2)',
  'rgba(255, 159, 64, 0.2)',
  'rgba(153, 102, 255, 0.2)',
  'rgba(75, 192, 192, 0.2)',
  'rgba(54, 162, 235, 0.2)',
]

const chartBorderColor = [
  'rgb(255, 99, 132)',
  'rgb(255, 159, 64)',
  'rgb(153, 102, 255)',
  'rgb(75, 192, 192)',
  'rgb(54, 162, 235)',
]

const cmdRate = shallowRef({
  labels: [],
  datasets: [
    {
      label: i18n.t('status.act_cmd'),
      data: [],
      fill: true,
      backgroundColor: chartBGColor[0],
      borderColor: chartBorderColor[0],
      tension: 0.4,
    },
  ],
})

const connectedClients = shallowRef({
  labels: [],
  datasets: [
    {
      label: i18n.t('status.connected_clients'),
      data: [],
      fill: true,
      backgroundColor: chartBGColor[1],
      borderColor: chartBorderColor[1],
      tension: 0.4,
    },
  ],
})

const memoryUsage = shallowRef({
  labels: [],
  datasets: [
    {
      label: i18n.t('status.memory_used'),
      data: [],
      fill: true,
      backgroundColor: chartBGColor[2],
      borderColor: chartBorderColor[2],
      tension: 0.4,
    },
  ],
})

const networkRate = shallowRef({
  labels: [],
  datasets: [
    {
      label: i18n.t('status.act_network_input'),
      data: [],
      fill: true,
      backgroundColor: chartBGColor[3],
      borderColor: chartBorderColor[3],
      tension: 0.4,
    },
    {
      label: i18n.t('status.act_network_output'),
      data: [],
      fill: true,
      backgroundColor: chartBGColor[4],
      borderColor: chartBorderColor[4],
      tension: 0.4,
    },
  ],
})

const chartOption = computed(() => {
  return {
    animation: false,
    responsive: true,
    maintainAspectRatio: false,
    events: [],
    scales: {
      x: {
        grid: {
          color: themeVars.value.borderColor,
        },
        ticks: {
          color: themeVars.value.textColor3,
        },
      },
      y: {
        beginAtZero: true,
        stepSize: 1024,
        suggestedMin: 0,
        grid: {
          color: themeVars.value.borderColor,
        },
        ticks: {
          color: themeVars.value.textColor3,
          precision: 0,
        },
      },
    },
    plugins: {
      legend: {
        labels: {
          color: themeVars.value.textColor2,
        },
      },
    },
  }
})

const byteChartOption = computed(() => {
  return {
    animation: false,
    responsive: true,
    maintainAspectRatio: false,
    events: [],
    scales: {
      x: {
        grid: {
          color: themeVars.value.borderColor,
        },
        ticks: {
          color: themeVars.value.textColor3,
        },
      },
      y: {
        beginAtZero: true,
        stepSize: 1024,
        suggestedMin: 0,
        grid: {
          color: themeVars.value.borderColor,
        },
        ticks: {
          color: themeVars.value.textColor3,
          precision: 0,
          // format display y axios tag
          callback: function (value, index, values) {
            return formatBytes(value, 1)
          },
        },
      },
    },
    plugins: {
      legend: {
        labels: {
          color: themeVars.value.textColor2,
        },
      },
    },
  }
})

const clientInfo = reactive({
  loading: false,
  content: [],
})
const onShowClients = async (show) => {
  if (show) {
    try {
      clientInfo.loading = true
      clientInfo.content = await browserStore.getClientList(props.server)
    } finally {
      clientInfo.loading = false
    }
  }
}

const clientTableColumns = computed(() => {
  return [
    {
      key: 'title',
      title: () => {
        return h(NSpace, { wrap: false, wrapItem: false, justify: 'center' }, () => [
          h('span', { style: { fontWeight: '550', fontSize: '15px' } }, i18n.t('status.client.title')),
          h(IconButton, {
            icon: Refresh,
            size: 16,
            onClick: () => onShowClients(true),
          }),
        ])
      },
      align: 'center',
      titleAlign: 'center',
      children: [
        {
          key: 'no',
          title: '#',
          width: 60,
          align: 'center',
          titleAlign: 'center',
          render: (row, index) => {
            return index + 1
          },
        },
        {
          key: 'addr',
          title: () => i18n.t('status.client.addr'),
          sorter: 'default',
          align: 'center',
          titleAlign: 'center',
        },
        {
          key: 'db',
          title: () => i18n.t('status.client.db'),
          align: 'center',
          titleAlign: 'center',
        },
        {
          key: 'age',
          title: () => i18n.t('status.client.age'),
          sorter: (row1, row2) => row1.age - row2.age,
          defaultSortOrder: 'descend',
          align: 'center',
          titleAlign: 'center',
          render: ({ age }, index) => {
            return toHumanReadable(age)
          },
        },
        {
          key: 'idle',
          title: () => i18n.t('status.client.idle'),
          sorter: (row1, row2) => row1.age - row2.age,
          align: 'center',
          titleAlign: 'center',
          render: ({ idle }, index) => {
            return toHumanReadable(idle)
          },
        },
      ],
    },
  ]
})
</script>

<template>
  <n-space :size="5" :wrap-item="false" style="padding: 5px; box-sizing: border-box; height: 100%" vertical>
    <n-card embedded>
      <template #header>
        <n-space :wrap-item="false" align="center" inline size="small">
          {{ props.server }}
          <n-tooltip v-if="redisVersion">
            Redis Version
            <template #trigger>
              <n-tag size="small" type="primary">v{{ redisVersion }}</n-tag>
            </template>
          </n-tooltip>
          <n-tooltip v-if="redisMode">
            Mode
            <template #trigger>
              <n-tag size="small" type="primary">{{ redisMode }}</n-tag>
            </template>
          </n-tooltip>
          <n-tooltip v-if="role">
            Role
            <template #trigger>
              <n-tag size="small" type="primary">{{ role }}</n-tag>
            </template>
          </n-tooltip>
        </n-space>
      </template>
      <template #header-extra>
        <n-popover keep-alive-on-hover placement="bottom-end" trigger="hover">
          <template #trigger>
            <n-button
              :loading="pageState.loading"
              :type="isLoading ? 'primary' : 'default'"
              circle
              size="small"
              tertiary
              @click="refreshInfo(true)">
              <template #icon>
                <n-icon :size="props.size">
                  <refresh
                    :class="{
                      'auto-rotate': pageState.autoRefresh || isLoading,
                    }"
                    :color="pageState.autoRefresh ? themeVars.primaryColor : undefined"
                    :stroke-width="pageState.autoRefresh ? 6 : 3" />
                </n-icon>
              </template>
            </n-button>
          </template>
          <auto-refresh-form
            v-model:interval="pageState.refreshInterval"
            v-model:on="pageState.autoRefresh"
            :default-value="5"
            :loading="pageState.autoLoading"
            @toggle="onToggleRefresh" />
        </n-popover>
      </template>
      <n-grid style="min-width: 500px" x-gap="5">
        <n-gi :span="6">
          <n-statistic :label="$t('status.uptime')" :value="uptime.value">
            <template #suffix>{{ $t(uptime.unit) }}</template>
          </n-statistic>
        </n-gi>
        <n-gi :span="6">
          <n-statistic
            :label="$t('status.connected_clients')"
            :value="get(serverInfo, 'Clients.connected_clients', '0')">
            <template #suffix>
              <n-tooltip
                :content-style="{ backgroundColor: themeVars.tableColor }"
                trigger="click"
                width="70vw"
                @update-show="onShowClients">
                <template #trigger>
                  <n-button :bordered="false" size="small">&LowerRightArrow;</n-button>
                </template>
                <n-data-table
                  :columns="clientTableColumns"
                  :data="clientInfo.content"
                  :loading="clientInfo.loading"
                  :single-column="false"
                  :single-line="false"
                  max-height="50vh"
                  size="small"
                  striped />
              </n-tooltip>
            </template>
          </n-statistic>
        </n-gi>
        <n-gi :span="6">
          <n-statistic :value="totalKeys">
            <template #label>
              {{ $t('status.total_keys') }}
            </template>
          </n-statistic>
        </n-gi>
        <n-gi :span="6">
          <n-statistic :label="$t('status.memory_used')" :value="usedMemory[0]">
            <template #suffix>{{ usedMemory[1] }}</template>
          </n-statistic>
        </n-gi>
      </n-grid>
    </n-card>
    <n-card class="flex-item-expand" content-style="padding: 0; height: 100%;" embedded style="overflow: hidden">
      <n-tabs
        v-model:value="tabVal"
        :tabs-padding="20"
        pane-style="padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; flex-grow: 1;"
        size="large"
        style="height: 100%; overflow: hidden"
        type="line">
        <template #suffix>
          <div v-if="tabVal === 'info'" style="padding-right: 10px">
            <n-input v-model:value="infoFilter.keyword" clearable placeholder="">
              <template #prefix>
                <icon-button :icon="Filter" size="18" />
              </template>
            </n-input>
          </div>
        </template>

        <!-- activity tab pane -->
        <n-tab-pane
          :tab="$t('status.activity_status')"
          class="line-chart"
          display-directive="show:lazy"
          name="activity">
          <div class="line-chart">
            <div class="line-chart-item">
              <Line :data="cmdRate" :options="chartOption" />
            </div>
            <div class="line-chart-item">
              <Line :data="connectedClients" :options="chartOption" />
            </div>
            <div class="line-chart-item">
              <Line :data="memoryUsage" :options="byteChartOption" />
            </div>
            <div class="line-chart-item">
              <Line :data="networkRate" :options="byteChartOption" />
            </div>
          </div>
        </n-tab-pane>

        <!-- info tab pane -->
        <n-tab-pane :tab="$t('status.server_info')" name="info">
          <n-space :wrap="false" :wrap-item="false" class="flex-item-expand">
            <n-space align="end" item-style="padding: 0 5px;" vertical>
              <n-button
                v-for="(v, k) in serverInfo"
                :key="k"
                :disabled="isEmpty(v)"
                :focusable="false"
                :type="infoFilter.group === k ? 'primary' : 'default'"
                secondary
                size="small"
                @click="onFilterGroup(k)">
                <span style="min-width: 80px">{{ k }}</span>
              </n-button>
            </n-space>
            <n-data-table
              :columns="[
                {
                  title: $t('common.key'),
                  key: 'key',
                  defaultSortOrder: 'ascend',
                  minWidth: 80,
                  titleAlign: 'center',
                  filterOptionValue: infoFilter.keyword,
                  filter(value, row) {
                    return !!~row.key.indexOf(value.toString())
                  },
                },
                { title: $t('common.value'), titleAlign: 'center', key: 'value' },
              ]"
              :data="info"
              :loading="pageState.loading"
              :single-line="false"
              class="flex-item-expand"
              flex-height
              striped />
          </n-space>
        </n-tab-pane>
      </n-tabs>
    </n-card>
  </n-space>
</template>

<style lang="scss" scoped>
@import '@/styles/content';

.line-chart {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  height: 100%;

  &-item {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: 50%;
    height: 50%;
    padding: 10px;
    box-sizing: border-box;
  }
}
</style>
